iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 18
0
Mobile Development

Android × CI/CD 如何用基本的MVVM專案實現 CI/CD 系列 第 18

Day18 MVVM專案-4 RequestPermission

  • 分享至 

  • xImage
  •  

今天延續Day16 MVVM專案-3 RecyclerView

有些權限在android6.0以上需要動態註冊

今天會寫個獲取聯絡人的功能來當範例

首先先去模擬器新增一個聯絡人
https://ithelp.ithome.com.tw/upload/images/20191003/20120279ETI2Xusbt3.png

然後開啟專案

AndroidManifest.xml

<application 
...
>
 <uses-permission android:name="android.permission.READ_CONTACTS" />

</application>

先宣告權限

新增viewModel

S04ViewModel.kt

class S04ViewModel : ViewModel() {

    val nameAndPhone = MutableLiveData<Pair<String, String>>()
    val contact: LiveData<String> = Transformations.map(nameAndPhone) { (name, phone) ->
        "Name : $name\nPhone : $phone"
    }
    val pickContactAction = MutableLiveData<Unit>()

    fun pickContact() {
        pickContactAction.value = Unit
    }
}

註冊權限的class

UserPermission.kt

class UserPermission(private val activity: Activity, vararg permissions: String) {
    val perms: Array<out String> by lazy { permissions }

    lateinit var isGrantedCallback: () -> Unit
    lateinit var notGrantedCallback: () -> Unit


    /**
     *  1. 確認權限是否註冊
     *  2. 如果註冊  呼叫`isGranted`, 或註冊權限
     *  3. 根據回傳結果呼叫 `isGranted` 或 `notGranted`
     */
    fun checkOrRequest(isGranted: () -> Unit, notGranted: () -> Unit) {
        val notGrantedPermissions = perms.filter {
            ContextCompat.checkSelfPermission(
                activity,
                it
            ) != PackageManager.PERMISSION_GRANTED
        }
            .toTypedArray()

        val permissionIsAllGranted = notGrantedPermissions.isEmpty()
        if (permissionIsAllGranted) {
            isGranted()
        } else {
            isGrantedCallback = isGranted
            notGrantedCallback = notGranted
            val requestCode =
                Math.abs((activity.hashCode() + isGranted.hashCode()).toShort().toInt())
            activity.permissions.put(requestCode, this)
            ActivityCompat.requestPermissions(activity, notGrantedPermissions, requestCode)
        }
    }

    override fun toString(): String = "${javaClass.simpleName} ${Arrays.toString(perms)}"
}

private val Activity.permissions by lazy { SparseArray<UserPermission>() }

fun Activity.permissionOf(vararg permissions: String): UserPermission =
    UserPermission(this, *permissions)


/**
 *  this has to be called in Activity.onRequestPermissionsResult
 */
fun Activity.requestPermissionResult(requestCode: Int, grantResults: IntArray) =
    permissions[requestCode]?.let {
        permissions.remove(requestCode)
        val isGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
        if (isGranted) it.isGrantedCallback() else it.notGrantedCallback()
    }

首頁
S04Activity.kt

class S04Activity : AppCompatActivity() {
    val permissRequestCode = 100
    private val viewModel: S04ViewModel by lazy {
        ViewModelProviders.of(this).get(S04ViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        DataBindingUtil.setContentView<ActivityS04Binding>(this, R.layout.activity_s04).let {
            it.setLifecycleOwner(this)
            it.viewModel = viewModel
        }

        viewModel.pickContactAction.observe(this, Observer {
            permissionOf(Manifest.permission.READ_CONTACTS).checkOrRequest({
                val intent =
                    Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
                startActivityForResult(intent, permissRequestCode)
            }, {
                Toast.makeText(this, "notGranted", Toast.LENGTH_SHORT).show()
            })
        })
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        requestPermissionResult(requestCode, grantResults)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == permissRequestCode && resultCode == Activity.RESULT_OK && data?.data != null) {

            val cursor = contentResolver.query(data.data!!, null, null, null, null)
            val nameAndPhone = cursor?.use {
                it.moveToFirst()

                val name =
                    it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME))
                val phone =
                    it.getString(it.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DATA))

                name to phone
            } ?: "" to ""

            viewModel.nameAndPhone.value = nameAndPhone
        }
    }
}

xml
activity_s04.xml

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
                name="viewModel"
                type="com.ithome11.jetpackmvvmdemo.main.s04.S04ViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">


        <androidx.appcompat.widget.AppCompatButton
                android:id="@+id/bt_pick_contact"
                android:layout_width="200dp"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:layout_marginEnd="8dp"
                android:layout_marginRight="8dp"
                android:onClick="@{_ -> viewModel.pickContact()}"
                android:text="pick contact"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/tv_contact"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="@{viewModel.contact}"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/bt_pick_contact" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

完成後的畫面
https://ithelp.ithome.com.tw/upload/images/20191003/20120279ndEHifJ21i.png

最後新增單元測試

class S04ViewModelTest {
    @Rule
    @JvmField
    val rule = InstantTaskExecutorRule()


    @Test
    fun pickContact() {
        val viewModel = S04ViewModel()
        val lifecycle = LifecycleRegistry(mockk()).apply {
            handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
        }

        // given
        val givenName = "abc"
        val givenPhone = "111"

        viewModel.contact.observe({ lifecycle }) {}
        viewModel.pickContactAction.observe({ lifecycle }) {
            viewModel.nameAndPhone.value = givenName to givenPhone
        }

        // when
        viewModel.pickContact()

        // then
        Assert.assertEquals("Name : $givenName\nPhone : $givenPhone", viewModel.contact.value)
    }
}

solution
https://github.com/mars1120/jetpackMvvmDemo/tree/mvvm-04-RequestPermission


上一篇
Day17 MVVM專案- 補單元測試
下一篇
Day19 MVVM專案-5 Fragments互動
系列文
Android × CI/CD 如何用基本的MVVM專案實現 CI/CD 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言